package com.jwsoft.manager.core.dao.service.impl;

import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.StrUtil;
import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.elasticsearch._types.InlineScript;
import co.elastic.clients.elasticsearch._types.ScriptBuilders;
import co.elastic.clients.elasticsearch._types.query_dsl.MatchQuery;
import co.elastic.clients.elasticsearch._types.query_dsl.Query;
import co.elastic.clients.elasticsearch._types.query_dsl.TermQuery;
import co.elastic.clients.elasticsearch.cluster.HealthResponse;
import co.elastic.clients.elasticsearch.core.DeleteByQueryResponse;
import co.elastic.clients.elasticsearch.core.SearchResponse;
import co.elastic.clients.elasticsearch.core.UpdateByQueryResponse;
import co.elastic.clients.elasticsearch.core.search.Hit;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.bifang.engine.assistant.core.exception.AppException;
import com.jwsoft.manager.common.enums.AreaEnum;
import com.jwsoft.manager.common.enums.ESAddressField;
import com.jwsoft.manager.common.vo.eduAddressTree.AddressEsVO;
import com.jwsoft.manager.common.vo.eduAddressTree.AddressRequest;
import com.jwsoft.manager.common.vo.eduAddressTree.AddressVO;
import com.jwsoft.manager.common.vo.eduAddressTree.ElasticHealthVO;
import com.jwsoft.manager.core.dao.service.AddressMessageService;
import com.jwsoft.manager.core.integration.eduAddressTree.impl.AddressTree;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;

import java.io.IOException;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * 地址格式化
 *
 * @author fangs
 */
@Slf4j
@Service
public class AddressMessageServiceImpl implements AddressMessageService {
    /**
     * 道路正则
     */
    private static final Pattern PATTERN_ROAD = Pattern.compile("[道路街巷]([\\d-]+号)?");

    /**
     * 道路编号正则
     */
    private static final Pattern PATTERN_ROAD_NUMBER = Pattern.compile("^([\\d-]+号)?");

    /**
     * 楼-单元-室正则
     */
    private static final Pattern PATTERN_HOUSE_UNIT_ROOM_NUMBER =
            Pattern.compile("([a-zA-Z\\d-]+[幢号栋])?([零一-九\\d]+单元)?([a-zA-Z\\d-]+室[、]?)*$");

    /**
     * 楼房号正则
     */
    private static final Pattern PATTERN_HOUSE = Pattern.compile("[a-zA-Z\\d-]+[幢号栋]");

    /**
     * 单元号正则
     */
    private static final Pattern PATTERN_UNIT = Pattern.compile("[零一-九\\d]+单元");

    /**
     * 房间号正则
     */
    private static final Pattern PATTERN_ROOM_NUMBER = Pattern.compile("([a-zA-Z\\d-]+室[、]?)+");

    /**
     * 村、社区正则
     */
    private static final Pattern PATTERN_VILLAGE = Pattern.compile("[\\s\\S]+(村|社区)");

    private static final List<String> AREA_TYPE_SORT = Arrays.asList("province", "city", "district", "town", "village", "road");

    private final static String ADDRESS_INDEX = "cloud_address_resolve";
    @Autowired
    private ElasticsearchClient elasticsearchClient;


    @Override
    public AddressVO parseAddressAuto(AddressRequest addressRequest) {
        //上下文对象，通过引用传递缓存地址各级数据
        Map<String, Object> parsedDataMap = new HashMap<>();
        String address = addressRequest.getAddress();
        //1.识别省
        address = parseProvince(address, parsedDataMap);

        //3.识别市
        address = parseCity(address, parsedDataMap);

        //5.识别区
        address = parseDistrict(address, parsedDataMap);
        //若未定位到具体区县 则不继续识别
        if (CollectionUtils.isEmpty(parsedDataMap)) {
            throw new AppException("无法从输入地址中定位到具体区县");
        }

        //6.识别乡/镇
        address = parseTown(address, parsedDataMap);

        //7.识别村、社区
        address = parseVillage(address, parsedDataMap);

        //获取道路名及道路编号
        address = parseRoad(address, parsedDataMap);

        //获取楼房编号
        getHouseNumber(address, parsedDataMap);

        //8.json转对象
        AddressVO addressVO = JSON.parseObject(JSON.toJSONString(parsedDataMap), AddressVO.class);
        addressVO.setOriginAdd(addressRequest.getAddress());
        return addressVO;
    }

    @Override
    public void saveAddressData(AddressEsVO addressEsVO) {
        if (addressEsVO.getAddressId() == null) {
            throw new AppException("addressId不能为空");
        }
        String areaType = addressEsVO.getAreaType();
        if (StringUtils.isEmpty(areaType)) {
            throw new AppException("areaType不能为空");
        }
        String id = addressEsVO.getAddressId().toString();
        Map<String, Object> data = Convert.convert(HashMap.class, addressEsVO);
        if (!data.containsKey(areaType + "Alias") || data.get(areaType + "Alias") == null) {
            throw new AppException("别名不能为空");
        }
        Map<String, Object> underlineData = new HashMap<>();
        Set<String> keySet = data.keySet();
        for (String s : keySet) {
            String underlineCase = StrUtil.toUnderlineCase(s);
            underlineData.put(underlineCase, data.get(s));
        }
        try {
            elasticsearchClient.index(builder -> builder.index(ADDRESS_INDEX).id(id).document(underlineData));
        } catch (IOException e) {
            throw new RuntimeException("es插入数据失败", e);
        }
    }

    @Override
    public Long deleteAddressByNode(String areaType, Long treeId) {
        String key = areaType + "_id";
        try {
            DeleteByQueryResponse deleteByQueryResponse = elasticsearchClient.deleteByQuery(builder -> builder.index(ADDRESS_INDEX)
                    .query(q -> q.bool(
                            b -> b.must(
                                    m -> m.term(t -> t.field(key).value(treeId))
                            )
                    ))
            );
            return deleteByQueryResponse.deleted();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public ElasticHealthVO checkElasticsearch() {
        ElasticHealthVO healthVO = new ElasticHealthVO();
        try {
            HealthResponse health = elasticsearchClient.cluster().health();
            String healthStr = health.toString();
            healthStr = healthStr.substring(healthStr.indexOf("{"));
            log.info("es健康状态:{}", healthStr);
            healthVO = Convert.convert(healthVO.getClass(), JSON.parse(healthStr));
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        return healthVO;
    }

    @Override
    public void updateAddressData(AddressEsVO addressEsVO) {
        if (addressEsVO.getAddressId() == null) {
            throw new AppException("addressId不能为空");
        }
        String areaType = addressEsVO.getAreaType();
        if (StringUtils.isBlank(areaType)) {
            throw new AppException("areaType不能为空");
        }
        String aliasName = areaType + "Alias";
        String idName = StrUtil.toUnderlineCase(areaType) + "_id";
        Map<String, Object> data = Convert.toMap(String.class, Object.class, addressEsVO);

        //别名不能为空
        if (!data.containsKey(aliasName) || data.get(aliasName) == null) {
            throw new AppException("别名不能为空");
        }
        //更新不更新id
        data.remove("addressId");
        Set<String> keySet = data.keySet();
        StringBuilder script = new StringBuilder();
        for (String s : keySet) {
            if (!ObjectUtils.isEmpty(data.get(s))) {
                String underlineCase = StrUtil.toUnderlineCase(s);
                Object o = data.get(s);
                if (o instanceof String) {
                    script.append("ctx._source.").append(underlineCase).append(" = ").append("'").append(data.get(s)).append("';");
                } else {
                    script.append("ctx._source.").append(underlineCase).append(" = ").append(data.get(s)).append(";");
                }

            }
        }
        InlineScript inlineScript = ScriptBuilders.inline().source(script.toString()).build();
        log.info("es更新数据脚本:{}", inlineScript);
        try {
            UpdateByQueryResponse updateByQueryResponse = elasticsearchClient.updateByQuery(builder ->
                    builder.index(ADDRESS_INDEX).query(
                                    q -> q.bool(
                                            b -> b.must(
                                                    m -> m.term(t -> t.field(idName).value(addressEsVO.getAddressId()))
                                            )
                                    )
                            )
                            .script(s -> s.inline(inlineScript))
            );
            log.info("es更新数据成功,更新条数:{}", updateByQueryResponse.updated());
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 识别省
     *
     * @param originAddress 地址
     * @param parsedDataMap 用于传递识别后的地址信息
     * @author fangs
     */
    public String parseProvince(String originAddress, Map<String, Object> parsedDataMap) {
        List<Query> queries = new ArrayList<>();
        //省 市 自治区
        String queryName = getAddressPrefix(originAddress, "省", "市", "自治区");
        Query provinceAlias = MatchQuery.of(m -> m.field("province_alias").query(queryName))._toQuery();
        queries.add(provinceAlias);

        String[] fetchField = {"province", "province_id", "province_code", "province_alias"};
        List<Map<String, Object>> search = this.search(fetchField, queries);
        for (Map<String, Object> map : search) {
            String address = checkAddress(originAddress, map, AreaEnum.province.getType());
            if (address != null) {
                parsedDataMap.putAll(map);
                return address;
            }
        }
        return originAddress;
    }

    /**
     * 识别地市级地址信息
     *
     * @param originAddress         地址
     * @param parsedProvinceDataMap 用于传递识别后的地址信息
     * @return 地址识别成功则截断地市地址并返回，否则返回原地址
     * @author fangs
     */
    private String parseCity(String originAddress, Map<String, Object> parsedProvinceDataMap) {
        String queryName = getAddressPrefix(originAddress, "市", "地区", "自治州");

        List<Query> queries = new ArrayList<>();
        String[] fetchField = {"province", "province_id", "province_code", "province_alias", "city", "city_id", "city_alias", "city_code"};
        if (parsedProvinceDataMap.containsKey(ESAddressField.province.getType())) {
            String province = String.valueOf(parsedProvinceDataMap.get(ESAddressField.province.getType()));
            Query provinceQuery = TermQuery.of(m -> m.field(ESAddressField.province.getType()).value(province))._toQuery();
            queries.add(provinceQuery);
        } else {
            fetchField = new String[]{"city", "city_id", "city_code", "city_alias", "province", "province_id", "province_code"};
        }

        Query matchQuery = MatchQuery.of(m -> m.field("city_alias").query(queryName))._toQuery();
        queries.add(matchQuery);

        List<Map<String, Object>> search = this.search(fetchField, queries);
        for (Map<String, Object> map : search) {
            String address = checkAddress(originAddress, map, AreaEnum.city.getType());
            if (address != null) {
                parsedProvinceDataMap.putAll(map);
                return address;
            }
        }
        return originAddress;
    }

    /**
     * 识别地址县级信息
     *
     * @param originAddress 地址串
     * @param contextMap    用于传递识别后的地址信息
     * @return 匹配成功返回截断后的地址，否则返回原地址
     * @author fangs
     */
    private String parseDistrict(String originAddress, Map<String, Object> contextMap) {
        String queryName = getAddressPrefix(originAddress, "县", "市", "区");
        List<Query> queries = new ArrayList<>();
        String[] fetchField = {"district_id", "district", "district_alias", "district_code"};
        if (contextMap.containsKey(ESAddressField.province.getType())) {
            String province = String.valueOf(contextMap.get(ESAddressField.province.getType()));
            Query provinceQuery = TermQuery.of(m -> m.field(ESAddressField.province.getType()).value(province))._toQuery();
            queries.add(provinceQuery);
        } else {
            fetchField = new String[]{"province_id", "province", "province_code", "district_id", "district", "district_alias", "district_code"};
        }
        if (contextMap.containsKey(ESAddressField.city.getType())) {
            getKeywordTermQuery("city", contextMap, queries);
        } else {
            fetchField = new String[]{"province_id", "province", "province_code", "city_id", "city", "city_code",
                    "district_id", "district", "district_code", "district_alias"};
        }

        Query matchQuery = MatchQuery.of(m -> m.field("district_alias").query(queryName))._toQuery();
        queries.add(matchQuery);

        List<Map<String, Object>> search = this.search(fetchField, queries);
        for (Map<String, Object> map : search) {
            String address = checkAddress(originAddress, map, AreaEnum.district.getType());
            if (address != null) {
                contextMap.putAll(map);
                return address;
            }
        }
        return originAddress;
    }

    /**
     * 识别乡镇、街道信息
     *
     * @param originAddress 地址
     * @param parsedDataMap 识别后的地址详细信息
     * @return 识别成功截断识别到的地址名
     * @author fangs
     */
    private String parseTown(String originAddress, Map<String, Object> parsedDataMap) {
        String queryName = getAddressPrefix(originAddress, "乡", "街道", "镇", "街道办事处");
        String[] fetchField = getFetchField(parsedDataMap, "town");

        List<Query> queries = new ArrayList<>();
        getKeywordTermQuery("district", parsedDataMap, queries);
        getKeywordTermQuery("province", parsedDataMap, queries);
        getKeywordTermQuery("city", parsedDataMap, queries);

        Query matchQuery = MatchQuery.of(builder -> builder.field("town_alias").query(queryName))._toQuery();
        queries.add(matchQuery);

        List<Map<String, Object>> search = this.search(fetchField, queries);
        for (Map<String, Object> map : search) {
            String address = checkAddress(originAddress, map, AreaEnum.town.getType());
            if (address != null) {
                parsedDataMap.putAll(map);
                return address;
            }
        }
        return originAddress;
    }

    /**
     * 识别村、社区信息
     *
     * @param clearTownAddress 地址
     * @param parsedDataMap    识别后的详细信息
     * @author fangs
     */
    private String parseVillage(String clearTownAddress, Map<String, Object> parsedDataMap) {
        String queryName = getAddressPrefix(clearTownAddress, "村", "社区");
        Matcher matcher = PATTERN_VILLAGE.matcher(clearTownAddress);
        if (matcher.find()) {
            queryName = clearTownAddress.substring(matcher.start(), matcher.end());
            //正则匹配到关键字 则将匹配到的字段作为默认村社区
            parsedDataMap.put(ESAddressField.village.getType(), queryName);
            parsedDataMap.put(ESAddressField.villageAlias.getType(), queryName);
        }
        String[] fetchField = getFetchField(parsedDataMap, "village");

        List<Query> queries = new ArrayList<>();
        getKeywordTermQuery("district", parsedDataMap, queries);
        getKeywordTermQuery("province", parsedDataMap, queries);
        getKeywordTermQuery("city", parsedDataMap, queries);
        getKeywordTermQuery("town", parsedDataMap, queries);


        String finalQueryName = queryName;
        Query matchQuery = MatchQuery.of(builder -> builder.field("village_alias").query(finalQueryName))._toQuery();
        queries.add(matchQuery);

        List<Map<String, Object>> search = this.search(fetchField, queries);
        for (Map<String, Object> map : search) {
            String address = checkAddress(clearTownAddress, map, AreaEnum.village.getType());
            if (address != null) {
                parsedDataMap.putAll(map);
                return address;
            }
        }
        return clearAddress(clearTownAddress, parsedDataMap, AreaEnum.village.getType());
    }

    /**
     * 从es查询数据
     *
     * @param fetchField 查询中需要显示的字段
     * @param queries    查询条件
     * @return 返回匹配度最高的1条数据
     * @author fangs
     */
    private List<Map<String, Object>> search(String[] fetchField, List<Query> queries) {
        try {
            SearchResponse<HashMap> response = elasticsearchClient.search(s -> s.index(ADDRESS_INDEX)
                            .query(q -> q.bool(b -> b.must(queries)))
                            .source(ss -> ss.filter(i -> i.includes(Arrays.asList(fetchField)))).size(3)
                    , HashMap.class);
            if (CollectionUtils.isEmpty(response.hits().hits())) {
                return new ArrayList<>();
            } else {
                List<Hit<HashMap>> hits = response.hits().hits();
                List<Map<String, Object>> list = new ArrayList<>(3);
                for (Hit<HashMap> hit : hits) {
                    list.add(hit.source());
                }
                return list;
            }
        } catch (IOException e) {
            throw new RuntimeException("ES查询数据失败", e);
        }
    }

    /**
     * 组精确查找语句，从data中获取数据
     *
     * @param key     字段名
     * @param data    数据
     * @param queries 查询条件
     */
    private void getKeywordTermQuery(String key, Map<String, Object> data, List<Query> queries) {
        if (data.containsKey(key)) {
            String val = String.valueOf(data.get(key));
            this.getKeywordTermQuery(key, val, queries);
        }
    }

    /**
     * 组精确查找语句
     *
     * @param key
     * @param val
     * @param queries
     */
    private void getKeywordTermQuery(String key, String val, List<Query> queries) {
        Query query = TermQuery.of(builder -> builder.field(key).value(val))._toQuery();
        queries.add(query);
    }

    private String clearAddress(String address, Map<String, Object> parsedDataMap, String areaType) {
        if (parsedDataMap.containsKey(areaType)) {
            String alias = String.valueOf(parsedDataMap.get(areaType + "_alias"));
            boolean cityFlag = false;
            for (String shortName : alias.split(AddressTree.SEARCH_TERM_SEPARATOR)) {
                int shortIndex = address.indexOf(shortName);
                if (shortIndex > -1) {
                    // 如果地址开头不是指定的名称，则去掉开头
                    if (shortIndex > 0) {
                        address = address.substring(shortIndex);
                    }
                    address = address.substring(shortName.length());
                    cityFlag = true;
                    break;
                }
            }
            if (!cityFlag) {
                parsedDataMap.clear();
            }
        }
        return address;
    }

    /**
     * 判断es中读取的数据是否匹配,匹配则将地址中对应地址截断并返回，否则返回null
     *
     * @param address       地址
     * @param parsedDataMap es数据
     * @param areaType      正在匹配的地址级别
     * @return 匹配则将地址中对应地址截断并返回，否则返回null
     * @author fangs
     */
    private String checkAddress(String address, Map<String, Object> parsedDataMap, String areaType) {
        if (parsedDataMap.containsKey(areaType)) {
            String alias = String.valueOf(parsedDataMap.get(areaType + "_alias"));
            String maxLenName = "";
            for (String shortName : alias.split(AddressTree.SEARCH_TERM_SEPARATOR)) {
                //获取关键字在地址中的位置
                int shortIndex = address.indexOf(shortName);
                if (shortIndex == 0 && shortName.length() > maxLenName.length()) {
                    maxLenName = shortName;
                }
            }
            if (ObjectUtils.isEmpty(maxLenName)) {
                return null;
            }
            return address.substring(maxLenName.length());
        }
        return null;
    }

    /**
     * 识别以指定关键字为结尾的前缀
     *
     * @return 以指定关键字为结尾的前缀
     */
    private String getAddressPrefix(String originAddress, String... keyword) {
        int provinceIndex = -1;
        String provinceEndName = null;
        for (String s : keyword) {
            provinceIndex = originAddress.indexOf(s);
            if (provinceIndex > -1) {
                provinceEndName = s;
                break;
            }
        }

        String queryName = originAddress;
        if (provinceIndex > -1) {
            queryName = originAddress.substring(0, provinceIndex + provinceEndName.length());
        }
        return queryName;
    }


    /**
     * 识别地址中的路和路编号
     *
     * @param address 地址 如：xx路1号xx小区
     * @param data    识别后的地址信息
     * @return 去除路和路编号的地址 如：xx路1号xx小区 则返回 xx小区
     * @author fangs
     */
    private String parseRoad(String address, Map<String, Object> data) {
        String queryName = address;
        //识别路编号
        Matcher matcher0 = PATTERN_ROAD.matcher(address);
        String roadNumber = null;
        String road = null;
        if (matcher0.find()) {
            queryName = address.substring(0, matcher0.start() + 1);
            road = address.substring(0, matcher0.start() + 1);
            roadNumber = address.substring(matcher0.start() + 1, matcher0.end());
            address = address.substring(matcher0.end());
        }
        data.put(AreaEnum.road.getType(), road);
        data.put(ESAddressField.roadAlias.getType(), road);
        data.put("roadNumber", roadNumber);

        //ES识别
        String[] fetchField = getFetchField(data, "road");

        List<Query> queries = new ArrayList<>();
        getKeywordTermQuery("district", data, queries);
        getKeywordTermQuery("province", data, queries);
        getKeywordTermQuery("city", data, queries);
        getKeywordTermQuery("town", data, queries);

        String finalQueryName = queryName;
        Query matchQuery = MatchQuery.of(builder -> builder.field("village_alias").query(finalQueryName))._toQuery();
        queries.add(matchQuery);

        List<Map<String, Object>> search = this.search(fetchField, queries);
        for (Map<String, Object> map : search) {
            String roadAddress = checkAddress(address, map, AreaEnum.road.getType());
            if (roadAddress != null) {
                data.putAll(map);
                Matcher matcher = PATTERN_ROAD_NUMBER.matcher(roadAddress);
                if (matcher.find()) {
                    roadNumber = address.substring(matcher.start(), matcher.end());
                    data.put("roadNumber", roadNumber);
                    address = address.substring(matcher0.end());
                }
                break;
            }
        }
        return address;
    }

    /**
     * 通过正则识别地址中的 幢、单元、门牌号信息
     *
     * @param address 地址，如：xx幢1单元101室
     * @param data    地址上下文对象
     * @author fangs
     */
    private void getHouseNumber(String address, Map<String, Object> data) {
        data.put(ESAddressField.detail.getType(), address);
        data.put(ESAddressField.detailAlias.getType(), address);
        String addressEnd = address;

        //先判断是否有格式化的 楼号-单元号-门牌号 格式，有则依次截取，反之直接结束
        Matcher matcher0 = PATTERN_HOUSE_UNIT_ROOM_NUMBER.matcher(addressEnd);
        if (matcher0.find() && matcher0.start() < addressEnd.length()) {
            data.put(ESAddressField.detail.getType(), addressEnd.substring(0, matcher0.start()));
            data.put(ESAddressField.detailAlias.getType(), addressEnd.substring(0, matcher0.start()));
            addressEnd = addressEnd.substring(matcher0.start());
        } else {
            return;
        }

        //识别楼号
        Matcher matcher = PATTERN_HOUSE.matcher(addressEnd);
        if (matcher.find()) {
            data.put("houseNumber", addressEnd.substring(matcher.start(), matcher.end()));
            addressEnd = addressEnd.substring(matcher.end());
        }
        //识别单元号
        Matcher matcher2 = PATTERN_UNIT.matcher(addressEnd);
        if (matcher2.find()) {
            data.put("unitNumber", addressEnd.substring(matcher2.start(), matcher2.end()));
            addressEnd = addressEnd.substring(matcher2.end());
        }
        //识别门牌号
        Matcher matcher3 = PATTERN_ROOM_NUMBER.matcher(addressEnd);
        if (matcher3.find()) {
            data.put("roomNumber", addressEnd.substring(matcher3.start(), matcher3.end()));
        }
    }


    /**
     * 获取ES查询的字段
     *
     * @param map      地址上下文对象
     * @param currType 当前识别的地址类型
     * @return ES查询的字段
     * @author fangs
     */
    private String[] getFetchField(Map<String, Object> map, String currType) {
        List<String> fetchFieldList = new ArrayList<>();
        for (String s : AREA_TYPE_SORT) {
            if (!map.containsKey(s) || s.equals(currType)) {
                fetchFieldList.addAll(Arrays.asList(new String[]{s + "_id", s, s + "_alias", s + "_code"}));
            }
            if (s.equals(currType)) {
                break;
            }
        }
        String[] fetchField = new String[fetchFieldList.size()];
        for (int i = 0; i < fetchFieldList.size(); i++) {
            fetchField[i] = fetchFieldList.get(i);
        }
        return fetchField;
    }


}
